Completed
Push — master ( 4fcad4...efcce7 )
by Muhammad Dyas
19s queued 16s
created

ActionHandler.recordVote   C

Complexity

Conditions 11

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 27
rs 5.4
c 0
b 0
f 0
cc 11

How to fix   Complexity   

Complexity

Complex classes like ActionHandler.recordVote often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1';
2
import BaseHandler from './BaseHandler';
3
import NewPollFormCard from '../cards/NewPollFormCard';
4
import {addOptionToState} from '../helpers/option';
5
import {callMessageApi} from '../helpers/api';
6
import {createDialogActionResponse, createStatusActionResponse} from '../helpers/response';
7
import PollCard from '../cards/PollCard';
8
import {PollState, Voter, Votes} from '../helpers/interfaces';
9
import AddOptionFormCard from '../cards/AddOptionFormCard';
10
import {saveVotes} from '../helpers/vote';
11
import {MAX_NUM_OF_OPTIONS} from '../config/default';
12
13
/*
14
This list methods are used in the poll chat message
15
 */
16
interface PollAction {
17
  saveOption(): Promise<chatV1.Schema$Message>;
18
19
  getEventPollState(): PollState;
20
}
21
22
export default class ActionHandler extends BaseHandler implements PollAction {
23
  async process(): Promise<chatV1.Schema$Message> {
24
    const action = this.event.common?.invokedFunction;
25
    switch (action) {
26
      case 'start_poll':
27
        return await this.startPoll();
28
      case 'vote':
29
        return this.recordVote();
30
      case 'add_option_form':
31
        return this.addOptionForm();
32
      case 'add_option':
33
        return await this.saveOption();
34
      case 'show_form':
35
        return {
36
          actionResponse: {
37
            type: 'DIALOG',
38
            dialogAction: {
39
              dialog: {
40
                body: new NewPollFormCard({topic: '', choices: []}).create(),
41
              },
42
            },
43
          },
44
        };
45
      default:
46
        return createStatusActionResponse('Unknown action!', 'UNKNOWN');
47
    }
48
  }
49
50
  /**
51
   * Handle the custom start_poll action.
52
   *
53
   * @returns {object} Response to send back to Chat
54
   */
55
  async startPoll() {
56
    // Get the form values
57
    const formValues = this.event.common?.formInputs;
58
    const topic = formValues?.['topic']?.stringInputs?.value?.[0]?.trim() ?? '';
59
    const isAnonymous = formValues?.['is_anonymous']?.stringInputs?.value?.[0] === '1';
60
    const allowAddOption = formValues?.['allow_add_option']?.stringInputs?.value?.[0] === '1';
61
    const choices = [];
62
    const votes: Votes = {};
63
64
    for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
65
      const choice = formValues?.[`option${i}`]?.stringInputs?.value?.[0]?.trim();
66
      if (choice) {
67
        choices.push(choice);
68
        votes[i] = [];
69
      }
70
    }
71
72
    if (!topic || choices.length === 0) {
73
      // Incomplete form submitted, rerender
74
      const dialog = new NewPollFormCard({
75
        topic,
76
        choices,
77
      }).create();
78
      return {
79
        actionResponse: {
80
          type: 'DIALOG',
81
          dialogAction: {
82
            dialog: {
83
              body: dialog,
84
            },
85
          },
86
        },
87
      };
88
    }
89
    const pollCard = new PollCard({
90
      topic: topic, choiceCreator: undefined,
91
      author: this.event.user,
92
      choices: choices,
93
      votes: votes,
94
      anon: isAnonymous,
95
      optionable: allowAddOption,
96
    }).createCardWithId();
97
    // Valid configuration, make the voting card to display in the space
98
    const message = {
99
      cardsV2: [pollCard],
100
    };
101
    const request = {
102
      parent: this.event.space?.name,
103
      requestBody: message,
104
    };
105
    const apiResponse = await callMessageApi('create', request);
106
    if (apiResponse) {
107
      return createStatusActionResponse('Poll started.', 'OK');
108
    } else {
109
      return createStatusActionResponse('Failed to start poll.', 'UNKNOWN');
110
    }
111
  }
112
113
  /**
114
   * Handle the custom vote action. Updates the state to record
115
   * the user's vote then rerenders the card.
116
   *
117
   * @returns {object} Response to send back to Chat
118
   */
119
  recordVote() {
120
    const parameters = this.event.common?.parameters;
121
    if (!(parameters?.['index'])) {
122
      throw new Error('Index Out of Bounds');
123
    }
124
    const choice = parseInt(parameters['index']);
125
    const userId = this.event.user?.name ?? '';
126
    const userName = this.event.user?.displayName ?? '';
127
    const voter: Voter = {uid: userId, name: userName};
128
    const state = JSON.parse(parameters['state']);
129
130
    // Add or update the user's selected option
131
    state.votes = saveVotes(choice, voter, state.votes, state.anon);
132
    const card = new PollCard(state);
133
    return {
134
      thread: this.event.message?.thread,
135
      actionResponse: {
136
        type: 'UPDATE_MESSAGE',
137
      },
138
      cardsV2: [card.createCardWithId()],
139
    };
140
  }
141
142
  /**
143
   * Opens and starts a dialog that allows users to add details about a contact.
144
   *
145
   * @returns {object} open a dialog.
146
   */
147
  addOptionForm() {
148
    const card = this.event.message!.cardsV2?.[0]?.card;
149
    // @ts-ignore: because too long
150
    const stateJson = (card.sections[0].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value || card.sections[1].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value) ?? '';
151
    const state = JSON.parse(stateJson);
152
    const dialog = new AddOptionFormCard(state).create();
153
    return createDialogActionResponse(dialog);
154
  };
155
156
  /**
157
   * Handle add new option input to the poll state
158
   * the user's vote then rerenders the card.
159
   *
160
   * @returns {object} Response to send back to Chat
161
   */
162
  async saveOption(): Promise<chatV1.Schema$Message> {
163
    const userName = this.event.user?.displayName ?? '';
164
    const state = this.getEventPollState();
165
    const formValues = this.event.common?.formInputs;
166
    const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || '';
167
    addOptionToState(optionValue, state, userName);
168
169
    const cardMessage = new PollCard(state).createMessage();
170
171
    const request = {
172
      name: this.event.message!.name,
173
      requestBody: cardMessage,
174
      updateMask: 'cardsV2',
175
    };
176
    const apiResponse = await callMessageApi('update', request);
177
    if (apiResponse) {
178
      return createStatusActionResponse('Option is added', 'OK');
179
    } else {
180
      return createStatusActionResponse('Failed to add option.', 'UNKNOWN');
181
    }
182
  }
183
184
  getEventPollState(): PollState {
185
    const parameters = this.event.common?.parameters;
186
    const state = parameters?.['state'];
187
    if (!state) {
188
      throw new ReferenceError('no valid state in the event');
189
    }
190
    return JSON.parse(state);
191
  }
192
}
193